eletrotupi / tcc/ frontend/app/triggers/[id].tsxd139cd140eae38d7f5b7000fa39a09c78d163f07
9.6 KB | 346 lines Raw
1
import { useState, useEffect, use } from "react";
2
import {
3
  StyleSheet,
4
  Text,
5
  View,
6
  ScrollView,
7
  Pressable,
8
  ActivityIndicator,
9
} from "react-native";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { format } from "date-fns";
12
import { ptBR } from "date-fns/locale";
13

14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
16
import { Badge } from "@/components/ui/Badge";
17
import { Button } from "@/components/ui/Button";
18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
19
import { Section, SectionHeader } from "@/components/ui/Sections";
20
import { Theme, Typography, Colors, Spacing } from "@/constants/theme";
21
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
22
import { getTriggerCategory } from "@/constants/trigger-categories";
23
import {
24
  useTrigger,
25
  useDeleteTrigger,
26
} from "@/hooks/useTrigger.queries";
27
import { formatMoment } from "@/lib/utils/time";
28
import { getMood } from "@/constants/moods";
29

30
const impactColor = (n: number) => {
31
  if (n <= 1) return '#86efac';
32
  if (n <= 2) return '#22c55e';
33
  if (n <= 3) return '#f59e0b';
34
  if (n <= 4) return '#f97316';
35
  return '#ef4444';
36
};
37

38
export default function TriggerDetailsScreen() {
39
  const { id } = useLocalSearchParams<{ id: string }>();
40
  const { data: trigger, isLoading, isError } = useTrigger(id);
41
  const deleteTrigger = useDeleteTrigger();
42
  const [minutesLeft, setMinutesLeft] = useState(0);
43
  const [canDelete, setCanDelete] = useState(false);
44

45
  useEffect(() => {
46
    const updateTimer = () => {
47
      const createdAt = new Date(trigger?.createdAt);
48
      if (!createdAt) return;
49

50
      const msSinceCreation = Date.now() - createdAt.getTime();
51
      const deletionWindowMs = 5 * 60 * 1000;
52

53
      const newCanDelete = msSinceCreation < deletionWindowMs;
54
      const newMinutesLeft = Math.max(
55
        0,
56
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
57
      );
58

59
      setCanDelete(newCanDelete);
60
      setMinutesLeft(newMinutesLeft);
61
    };
62

63
    if (isLoading) return;
64

65
    updateTimer(); // Set initial values
66

67
    const interval = setInterval(updateTimer, 1000);
68

69
    return () => clearInterval(interval);
70
  }, [trigger, isLoading]);
71

72
  if (isLoading) {
73
    return (
74
      <View style={styles.centered}>
75
        <ActivityIndicator size="large" color={Colors.light.tint} />
76
      </View>
77
    );
78
  }
79

80
  if (isError || !trigger) {
81
    return (
82
      <View style={styles.centered}>
83
        <Text style={styles.errorText}>Registro não encontrado.</Text>
84
        <Pressable onPress={() => router.back()} style={styles.backButton}>
85
          <Text style={styles.backButtonText}>Voltar</Text>
86
        </Pressable>
87
      </View>
88
    );
89
  }
90

91
  const meta = getTriggerCategory(trigger.category);
92

93
  const handleDelete = async () => {
94
    await deleteTrigger.mutateAsync(String(trigger.id));
95

96
    router.back();
97
  };
98

99
  const deleteButtonMsg = () => {
100
    const msg = "Excluir Registro";
101
    const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
102
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
103

104
    return `${msg} ${suffix}`;
105
  };
106

107
  return (
108
    <>
109
      <Stack.Screen options={{ title: "Ver detalhes" }} />
110

111
      <ScrollView
112
        style={styles.container}
113
        contentContainerStyle={styles.content}
114
        showsVerticalScrollIndicator={false}
115
      >
116
        {/* Header card */}
117
        <Card style={styles.headerCard}>
118
          <Row style={{ alignItems: "center" }}>
119
            <View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
120
              <MaterialIcons name={meta.icon} size={22} color={meta.color} />
121
            </View>
122

123
            <Col gap={2}>
124
              <Text style={styles.triggerLabel}>
125
                {meta.label}
126
              </Text>
127
              <Text style={styles.triggerMoment}>
128
                {formatMoment(new Date(trigger.moment))}
129
              </Text>
130
            </Col>
131
          </Row>
132
        </Card>
133

134
        {/* Linked moods */}
135
        <Section>
136
          <SectionHeader title="Vínculo" variant="subtle" />
137
          {trigger.moodLinks && trigger.moodLinks.length > 0 ? (
138
            <Col gap={8}>
139
              {trigger.moodLinks.map((link) => {
140
                const moodDef = getMood(link.mood.selectedMood.toLowerCase());
141
                return (
142
                  <Pressable
143
                    key={link.id}
144
                    onPress={() => router.push(`/entry/${link.mood.id}`)}
145
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
146
                  >
147
                    <Card style={styles.linkCard}>
148
                      <Row style={{ alignItems: "center", gap: 12 }}>
149
                        <View style={styles.linkIconCircle}>
150
                          <Text style={styles.linkMoodEmoji}>{moodDef?.icon ?? "😐"}</Text>
151
                        </View>
152
                        <Col gap={3} style={{ flex: 1 }}>
153
                          <Row style={{ alignItems: "center", gap: 6 }}>
154
                            <Text style={styles.linkLabel}>
155
                              {moodDef?.label ?? link.mood.selectedMood}
156
                            </Text>
157

158
                            <Badge
159
                              label="Humor"
160
                              variant="yellow"
161
                              style={undefined}
162
                            />
163
                          </Row>
164

165
                          <Text style={styles.linkTime}>
166
                            {formatMoment(new Date(link.mood.moment))}
167
                          </Text>
168
                        </Col>
169
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
170
                      </Row>
171
                      <View style={styles.impactBar}>
172
                        <View
173
                          style={[
174
                            styles.impactFill,
175
                            {
176
                              width: `${(link.perceivedImpact / 5) * 100}%`,
177
                              backgroundColor: impactColor(link.perceivedImpact),
178
                            },
179
                          ]}
180
                        />
181
                      </View>
182
                    </Card>
183
                  </Pressable>
184
                );
185
              })}
186
            </Col>
187
          ) : (
188
            <NoLinkMessage />
189
          )}
190
        </Section>
191

192
        {/* Annotation / notes */}
193
        {trigger.comment && (
194
          <Section>
195
            <SectionHeader title="Anotações" variant="subtle" />
196
            <Card style={{ padding: Spacing.cardGap }}>
197
              <Text style={styles.annotation}>
198
                "{trigger.comment?.trim()}"
199
              </Text>
200
            </Card>
201
          </Section>
202
        )}
203

204
        {/* Delete */}
205
        <View style={styles.section}>
206
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
207
          <Button
208
            variant="outline"
209
            isLoading={false}
210
            disabled={!canDelete}
211
            onPress={() => router.push(`/triggers/edit/${trigger.id}`)}
212
            title="Editar Registro"
213
          />
214
          <Button
215
            variant="danger"
216
            isLoading={deleteTrigger.isPending}
217
            disabled={!canDelete || deleteTrigger.isPending}
218
            onPress={handleDelete}
219
            title={deleteButtonMsg()}
220
          />
221

222
          <SubtleInfoCard
223
            style={styles.deleteInfo}
224
            text={`
225
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
226
              após o envio para garantir a integridade do histórico emocional.
227
              `}
228
          />
229
        </View>
230
      </ScrollView>
231
    </>
232
  );
233
}
234

235
const styles = StyleSheet.create({
236
  container: {
237
    flex: 1,
238
    backgroundColor: Theme.colors.background,
239
  },
240
  content: {
241
    padding: Spacing.containerPadding,
242
    gap: Spacing.sectionGap,
243
    paddingBottom: 48,
244
  },
245
  iconWrap: {
246
    width: 40,
247
    height: 40,
248
    borderRadius: 20,
249
    alignItems: 'center',
250
    justifyContent: 'center',
251
    marginTop: 1
252
  },
253
  headerCardContent: {
254
    flexDirection: "row",
255
    alignItems: "center",
256
    gap: 8,
257
  },
258
  triggerLabel: {
259
    ...Typography.headlineLg,
260
    color: Colors.light.text,
261
  },
262
  centered: {
263
    flex: 1,
264
    justifyContent: "center",
265
    alignItems: "center",
266
    gap: 12,
267
  },
268
  errorText: {
269
    fontSize: 15,
270
    color: Colors.light.textSecondary,
271
  },
272
  backButton: {
273
    paddingHorizontal: 20,
274
    paddingVertical: 10,
275
    backgroundColor: Theme.colors.tint,
276
    borderRadius: Theme.borderRadius.md,
277
  },
278
  backButtonText: {
279
    fontWeight: "700",
280
    color: Theme.colors.text,
281
  },
282

283
  // Header
284
  headerCard: {
285
    padding: 16,
286
  },
287
  triggerMoment: {
288
    fontSize: 13,
289
    color: Colors.light.textSecondary,
290
  },
291

292
  // Sections
293
  section: {
294
    gap: 8,
295
  },
296
  sectionLabel: {
297
    fontSize: 11,
298
    fontWeight: "600",
299
    letterSpacing: 0.8,
300
    color: Colors.light.textSecondary,
301
    textTransform: "uppercase",
302
  },
303

304
  // Annotation
305
  annotation: {
306
    fontSize: 14,
307
    lineHeight: 22,
308
    color: Colors.light.textSecondary,
309
    fontStyle: "italic",
310
    padding: 4,
311
  },
312
  // Linked mood cards
313
  linkCard: {
314
    padding: Spacing.cardGap,
315
    overflow: "hidden",
316
    gap: 10,
317
  },
318
  linkIconCircle: {
319
    width: 44,
320
    height: 44,
321
    alignItems: "center",
322
    justifyContent: "center",
323
  },
324
  linkMoodEmoji: {
325
    fontSize: 28,
326
  },
327
  linkTime: {
328
    fontSize: 12,
329
    color: Colors.light.textSecondary,
330
  },
331
  linkLabel: {
332
    fontSize: 15,
333
    fontWeight: "700",
334
    color: Colors.light.text,
335
  },
336
  impactBar: {
337
    height: 3,
338
    backgroundColor: Colors.light.divider,
339
    borderRadius: 99,
340
    overflow: "hidden",
341
  },
342
  impactFill: {
343
    height: "100%",
344
    borderRadius: 99,
345
  },
346
});